/* uc65 - Micro Code for 6502/NES
 * Copyright (C) 2013  Norman B. Lancaster
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.gmail.qbradq.uc65;

import java.util.EnumSet;
import java.util.Set;

/**
 * Implements a 6502 machine code optimizer using a state machine representation
 * of the software.
 */
public class Optimizer {
	private static class RegisterValue {
		private AddressMode mode;
		private String ref;
		private int value;
		private RegisterValue x;
		private RegisterValue y;
		
		public RegisterValue(AddressMode mode, String ref, int value, RegisterValue x, RegisterValue y) {
			this.mode = mode;
			this.ref = ref;
			this.value = value;
			this.x = x;
			this.y = y;
		}
		
		public boolean isEquivalent(RegisterValue r) {
			if(mode == null || r.mode == null) {
				return false;
			}
			if(mode != r.mode || value != r.value) {
				return false;
			}
			if(ref == null && r.ref != null) {
				return false;
			}
			if(r.ref == null && ref != null) {
				return false;
			}
			if(ref != null && !ref.equals(r.ref)) {
				return false;
			}
			if(mode == AddressMode.ADDRESSX && !x.isEquivalent(r.x)) {
				return false;
			}
			if(mode == AddressMode.ADDRESSY && !y.isEquivalent(r.y)) {
				return false;
			}
			// We don't do pointer tracking
			if(mode == AddressMode.INDIRECT || mode == AddressMode.INDIRECTX || mode == AddressMode.INDIRECTY) {
				return false;
			}
			return true;
		}
	}
	
	private static final Set<Operation> invalidatesAImplied = EnumSet.of(
		Operation.ASL,
		Operation.DEC,
		Operation.INC,
		Operation.LSR,
		Operation.ROL,
		Operation.ROR
	);
	private static final Set<Operation> invalidatesA = EnumSet.of(
		Operation.ADC,
		Operation.AND,
		Operation.EOR,
		Operation.ORA,
		Operation.PLA,
		Operation.SBC
	);
	private static final Set<Operation> invalidatesX = EnumSet.of(
		Operation.DEX,
		Operation.INX,
		Operation.TSX
	);
	private static final Set<Operation> invalidatesY = EnumSet.of(
		Operation.DEY,
		Operation.INY
	);

	
	private static final RegisterValue unknown = new RegisterValue(null, null, 0, null, null);
	private RegisterValue x = unknown;
	private RegisterValue y = unknown;
	private RegisterValue a = unknown;
	
	/**
	 * Resets the optimizer.
	 * 
	 * Call this every time the state of the machine leaves a known state, such
	 * as after a jump or branch.
	 */
	public void reset() {
		x = y = a = unknown;
	}
	
	/**
	 * Call for every machine instruction emitted.
	 * 
	 * @param op The operation being performed
	 * @param mode The addressing mode of the operation
	 * @param ref For address modes this is the name of the reference.
	 * @param value For immediate mode this is the value.
	 * @return True if the instruction must be emitted to the output stream,
	 * false if it is redundant.
	 */
	public boolean instruction(Operation op, AddressMode mode, String ref, int value) {
		switch(op) {
			case TAX:
				if(x.isEquivalent(a)) {
					return false;
				}
				x = a;
				break;
			case TAY:
				if(y.isEquivalent(a)) {
					return false;
				}
				y = a;
				break;
			case TXA:
				if(a.isEquivalent(x)) {
					return false;
				}
				a = x;
				break;
			case TYA:
				if(a.isEquivalent(y)) {
					return false;
				}
				a = y;
				break;
			case LDA:
				RegisterValue newA = new RegisterValue(mode, ref, value, x, y);
				if(a.isEquivalent(newA)) {
					return false;
				}
				a = new RegisterValue(mode, ref, value, x, y);
				break;
			case LDX:
				RegisterValue newX = new RegisterValue(mode, ref, value, x, y);
				if(x.isEquivalent(newX)) {
					return false;
				}
				x = new RegisterValue(mode, ref, value, x, y);
				break;
			case LDY:
				RegisterValue newY = new RegisterValue(mode, ref, value, x, y);
				if(y.isEquivalent(newY)) {
					return false;
				}
				y = new RegisterValue(mode, ref, value, x, y);
				break;
			default:
				if(mode == AddressMode.IMPLICIT && invalidatesAImplied.contains(op)) {
					a = unknown;
				}
				if(invalidatesA.contains(op)) {
					a = unknown;
				}
				if(invalidatesX.contains(op)) {
					x = unknown;
				}
				if(invalidatesY.contains(op)) {
					y = unknown;
				}
				break;
		}
		return true;
	}
}
